home *** CD-ROM | disk | FTP | other *** search
/ Chip 2006 June / CHIP 2006-06.2.iso / program / freeware / Democracy-0.8.2.exe / xulrunner / python / item.py < prev    next >
Encoding:
Python Source  |  2006-04-10  |  28.8 KB  |  970 lines

  1. from datetime import datetime, timedelta
  2. from database import DDBObject, defaultDatabase
  3. from downloader import DownloaderFactory
  4. from copy import copy
  5. from xhtmltools import unescape,xhtmlify
  6. from xml.sax.saxutils import unescape
  7. from scheduler import ScheduleEvent
  8. from feedparser import FeedParserDict
  9. from threading import Thread
  10. from math import ceil
  11. from templatehelper import escape
  12. import threadpriority
  13. import config
  14. import os
  15. import feed
  16.  
  17. ##
  18. # An item corresponds to a single entry in a feed. Generally, it has
  19. # a single url associated with it
  20. class Item(DDBObject):
  21.     manualDownloads = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.getState() == "downloading" and not x.getAutoDownloaded())
  22.  
  23.     def __init__(self, feed, entry, linkNumber = 0):
  24.         self.feed = feed
  25.         self.seen = False
  26.         self.downloaders = []
  27.         self.vidinfo = None
  28.         self.autoDownloaded = False
  29.         self.startingDownload = False
  30.         self.lastDownloadFailed = False
  31.         self.pendingManualDL = False
  32.         self.pendingReason = ""
  33.         self.entry = entry
  34.         self.dlFactory = DownloaderFactory(self)
  35.         self.expired = False
  36.         self.keep = False
  37.         # linkNumber is a hack to make sure that scraped items at the
  38.         # top of a page show up before scraped items at the bottom of
  39.         # a page. 0 is the topmost, 1 is the next, and so on
  40.         self.linkNumber = linkNumber
  41.         self.creationTime = datetime.now()
  42.         DDBObject.__init__(self)
  43.  
  44.     # Unfortunately, our database does not scale well with many views,
  45.     # so we have this hack to make sure that unwatched and available
  46.     # get updated when an item changes
  47.     def endChange(self):
  48.         DDBObject.endChange(self)
  49.         self.feed.updateUandA()
  50.  
  51.     #
  52.     # Returns True iff this item has never been viewed in the interface
  53.     # Note the difference between "viewed" and seen
  54.     def getViewed(self):
  55.         return self.creationTime <= self.feed.lastViewed
  56.  
  57.     ##
  58.     # Returns the URL associated with the first enclosure in the item
  59.     def getURL(self):
  60.         ret = ''
  61.         self.beginRead()
  62.         try:
  63.             try:
  64.                 ret = self.entry.enclosures[0].url
  65.             except:
  66.                 pass
  67.         finally:
  68.             self.endRead()
  69.         return ret
  70.     ##
  71.     # Returns the feed this item came from
  72.     def getFeed(self):
  73.         self.beginRead()
  74.         ret = self.feed
  75.         self.endRead()
  76.         return ret
  77.  
  78.     ##
  79.     # Returns the number of videos associated with this item
  80.     def getAvailableVideos(self):
  81.         ret = 0
  82.         self.beginRead()
  83.         try:
  84.             ret = len(self.entry.enclosures)
  85.         finally:
  86.             self.endRead()
  87.         return ret
  88.  
  89.     ##
  90.     # Marks this item as expired
  91.     def expire(self):
  92.         self.beginRead()
  93.         try:
  94.             self.stopDownload()
  95.             # FIXME: should expired items be marked as "seen?"
  96.             # self.markItemSeen()
  97.             self.expired = True
  98.         finally:
  99.             self.endRead()        
  100.         self.beginChange()
  101.         self.endChange()
  102.  
  103.     ##
  104.     # Returns string with days or hours until this gets deleted
  105.     def getExpirationTime(self):
  106.         ret = "???"
  107.         self.beginRead()
  108.         self.feed.beginRead()
  109.         try:
  110.             if self.feed.expire == 'never' or (self.feed.expire == 'system' and config.get(config.EXPIRE_AFTER_X_DAYS) <= 0):
  111.                 ret = "never"
  112.             else:
  113.                 if self.feed.expire == "feed":
  114.                     expireTime = self.feed.expireTime
  115.                 elif self.feed.expire == "system":
  116.                     expireTime = timedelta(days=config.get(config.EXPIRE_AFTER_X_DAYS))
  117.                 
  118.                 exp = expireTime - (datetime.now() - self.getDownloadedTime())
  119.                 if exp.days > 0:
  120.                     ret = "%d days" % exp.days
  121.                 elif exp.seconds > 3600:
  122.                     ret = "%d hours" % (ceil(exp.seconds/3600.0))
  123.                 else:
  124.                     ret = "%d min." % (ceil(exp.seconds/60.0))
  125.         finally:
  126.             self.feed.endRead()
  127.             self.endRead()
  128.         return ret
  129.  
  130.     def getKeep(self):
  131.         self.beginRead()
  132.         ret = self.keep
  133.         self.endRead()
  134.         return ret
  135.  
  136.     def setKeep(self,val):
  137.         self.beginRead()
  138.         self.keep = val
  139.         self.endRead()
  140.         self.beginChange()
  141.         self.endChange()
  142.  
  143.     ##
  144.     # returns true iff video has been seen
  145.     # Note the difference between "viewed" and "seen"
  146.     def getSeen(self):
  147.         self.beginRead()
  148.         ret = self.seen
  149.         self.endRead()
  150.         return ret
  151.  
  152.     ##
  153.     # Marks the item as seen
  154.     def markItemSeen(self):
  155.         self.beginChange()
  156.         try:
  157.             self.seen = True
  158.         finally:
  159.             self.endChange()
  160.  
  161.     ##
  162.     # Returns a list of downloaders associated with this object
  163.     def getDownloaders(self):
  164.         self.beginRead()
  165.         ret = self.downloaders
  166.         self.endRead()
  167.         return ret
  168.  
  169.     def getRSSID(self):
  170.         self.beginRead()
  171.         try:
  172.             ret = self.entry["id"]
  173.         finally:
  174.             self.endRead()
  175.         return ret
  176.  
  177.     ##
  178.     # Returns the vidinfo object associated with this item
  179.     def getVidInfo(self):
  180.         self.beginRead()
  181.         ret = self.vidinfo
  182.         self.endRead()
  183.         return ret
  184.  
  185.     def setAutoDownloaded(self,autodl = True):
  186.         self.beginRead()
  187.         self.autoDownloaded = autodl
  188.         self.endRead()
  189.  
  190.     def getPendingReason(self):
  191.         ret = ""
  192.         self.beginRead()
  193.         ret = self.pendingReason
  194.         self.endRead()
  195.         return ret
  196.  
  197.     ##
  198.     # Returns true iff item was auto downloaded
  199.     def getAutoDownloaded(self):
  200.         self.beginRead()
  201.         ret = self.autoDownloaded
  202.         self.endRead()
  203.         return ret
  204.  
  205.     ##
  206.     # Returns the linkNumber
  207.     def getLinkNumber(self):
  208.         self.beginRead()
  209.         try:
  210.             ret = self.linkNumber
  211.         finally:
  212.             self.endRead()
  213.         return ret
  214.  
  215.     def download(self,autodl=False):
  216.         thread = Thread(target = lambda:self.actualDownload(autodl),
  217.                         name = "Item download -- %s" % (self.getURL(), ))
  218.         thread.setDaemon(False)
  219.         thread.start()
  220.  
  221.     ##
  222.     # Starts downloading the item
  223.     def actualDownload(self,autodl=False):
  224.         threadpriority.setNormalPriority()
  225.         spawn = True
  226.         self.beginRead()
  227.         try:
  228.             # FIXME: For locking reasons, downloaders don't always
  229.             #        call beginChange() and endChange(), so we have to
  230.             #        recompute this filter
  231.             defaultDatabase.recomputeFilter(self.manualDownloads)
  232.             if ((not autodl) and 
  233.                 self.manualDownloads.len() >= config.get(config.MAX_MANUAL_DOWNLOADS)):
  234.                 self.pendingManualDL = True
  235.                 self.pendingReason = "Too many manual downloads"
  236.                 spawn = False
  237.                 self.expired = False
  238.             else:
  239.                 #Don't spawn two downloaders
  240.                 if self.startingDownload:
  241.                     spawn = False
  242.                 else:
  243.                     self.setAutoDownloaded(autodl)
  244.                     self.expired = False
  245.                     self.keep = False
  246.                     self.pendingManualDL = False
  247.                     self.lastDownloadFailed = False
  248.                     downloadURLs = map(lambda x:x.getURL(),self.downloaders)
  249.                     self.startingDownload = True
  250.             try:
  251.                 enclosures = self.entry["enclosures"]
  252.             except:
  253.                 enclosures = []
  254.         finally:
  255.             self.endRead()
  256.         self.beginChange()
  257.         self.endChange()
  258.  
  259.         if not spawn:
  260.             return
  261.  
  262.         try:
  263.             for enclosure in enclosures:
  264.                 try:
  265.                     if not enclosure["url"] in downloadURLs:
  266.                         dler = self.dlFactory.getDownloader(enclosure["url"])
  267.                         if dler != None:
  268.                             self.beginRead()
  269.                             try:
  270.                                 self.downloaders.append(dler)
  271.                             finally:
  272.                                 self.endRead()
  273.                         else:
  274.                             self.beginRead()
  275.                             try:
  276.                                 self.lastDownloadFailed = True
  277.                             finally:
  278.                                 self.endRead()
  279.                         downloadURLs.append(dler.getURL())
  280.                     else:
  281.                         for dler in self.downloaders:
  282.                             if dler.getURL() == enclosure['url']:
  283.                                 dler.start()
  284.                 except KeyError:
  285.                     pass
  286.         except KeyError:
  287.             pass
  288.         self.beginRead()
  289.         try:
  290.             self.startingDownload = False
  291.         finally:
  292.             self.endRead()
  293.         self.beginChange()
  294.         self.endChange()
  295.  
  296.  
  297.     ##
  298.     # Returns a link to the thumbnail of the video
  299.     def getThumbnail(self):
  300.         ret = None
  301.         self.beginRead()
  302.         try:
  303.             if self.entry.has_key('enclosures'):
  304.                 for enc in self.entry.enclosures:
  305.                     if enc.has_key('thumbnail') and enc['thumbnail'].has_key('url'):
  306.                         ret = enc["thumbnail"]["url"]
  307.                         break
  308.             if (ret is None and self.entry.has_key('thumbnail') and
  309.                 self.entry['thumbnail'].has_key('url')):
  310.                 ret =  self.entry["thumbnail"]["url"]
  311.         finally:
  312.             self.endRead()
  313.         if ret is None or not (ret.startswith('http:') or
  314.                                 ret.startswith('https:')):
  315.             ret = "resource:images/thumb.png"
  316.         return ret
  317.     ##
  318.     # returns the title of the item
  319.     def getTitle(self):
  320.         try:
  321.             return self.entry.title
  322.         except:
  323.             try:
  324.                 return self.entry.enclosures[0]["url"]
  325.             except:
  326.                 return ""
  327.  
  328.     ##
  329.     # Returns valid XHTML containing a description of the video
  330.     def getDescription(self):
  331.         self.beginRead()
  332.         try:
  333.             ret = xhtmlify('<span>'+unescape(self.entry.enclosures[0]["text"])+'</span>')
  334.         except:
  335.             try:
  336.                 ret = xhtmlify('<span>'+unescape(self.entry.description)+'</span>')
  337.             except:
  338.                 ret = '<span />'
  339.         self.endRead()
  340.         return ret
  341.  
  342.     ##
  343.     # Returns formatted XHTML with release date, duration, format, and size
  344.     def getDetails(self):
  345.         details = []
  346.         reldate = self.getReleaseDate()
  347.         duration = self.getDuration()
  348.         format = self.getFormat()
  349.         size = self.getSizeForDisplay()
  350.         if len(reldate) > 0:
  351.             details.append('<span class="details-date">%s</span>' % escape(reldate))
  352.         if len(duration) > 0:
  353.             details.append('<span class="details-duration">%s</span>' % escape(duration))
  354.         if len(format) > 0:
  355.             details.append('<span class="details-format">%s</span>' % escape(format))
  356.         if len(size) > 0:
  357.             details.append('<span class="details-size">%s</span>' % escape(size))
  358.         out = ' - '.join(details)
  359.         return '<div class="main-video-details-under">%s</div>' % out
  360.  
  361.     ##
  362.     # Stops downloading the item
  363.     def stopDownload(self):
  364.         for dler in self.downloaders:
  365.             dler.stop()
  366.             dler.remove()
  367.         self.beginRead()
  368.         try:
  369.             self.downloaders = []
  370.             self.keep = False
  371.             self.pendingManualDL = False
  372.         finally:
  373.             self.endRead()
  374.  
  375.     ##
  376.     # returns status of the download in plain text
  377.     def getState(self):
  378.         self.beginRead()
  379.         self.feed.beginRead()
  380.         try:
  381.             state = self.getStateNoAuto()
  382.             if ((state == "stopped") and 
  383.                 self.feed.isAutoDownloadable() and 
  384.                 (self.feed.getEverything or 
  385.                  self.getPubDateParsed() >= self.feed.startfrom)):
  386.                 state = "autopending"
  387.         finally:
  388.             self.feed.endRead()
  389.             self.endRead()
  390.             
  391.         return state
  392.     
  393.  
  394.     ##
  395.     # returns the state of the download, without checking automatic dl
  396.     # eligibility
  397.     def getStateNoAuto(self):
  398.         self.beginRead()
  399.         try:
  400.             if self.expired:
  401.                 state = "expired"
  402.             elif self.startingDownload:
  403.                 state = "downloading"
  404.             elif self.keep:
  405.                 state = "saved"
  406.             elif self.pendingManualDL:
  407.                 state = "manualpending"
  408.             elif len(self.downloaders) == 0:
  409.                 if self.lastDownloadFailed:
  410.                     state = "failed"
  411.                 else:
  412.                     state = "stopped"
  413.             else:
  414.                 state = "finished"
  415.                 for dler in self.downloaders:
  416.                     newState = dler.getState()
  417.                     if newState != "finished":
  418.                         state = newState
  419.                     if state == "failed":
  420.                         break
  421.             if (state == "finished" or state=="uploading") and self.seen:
  422.                 state = "watched"
  423.         finally:
  424.             self.endRead()
  425.         return state
  426.  
  427.     def getFailureReason(self):
  428.         ret = ""
  429.         self.beginRead()
  430.         try:
  431.             if self.lastDownloadFailed:
  432.                 ret = "Could not connect to server"
  433.             else:
  434.                 for dler in self.downloaders:
  435.                     if dler.getState() == "failed":
  436.                         ret = dler.getReasonFailed()
  437.                         break
  438.         finally:
  439.             self.endRead()
  440.         return ret
  441.     
  442.     ##
  443.     # Returns the size of the item to be displayed. If the item has a corresponding
  444.     # downloaded enclosure we use the pysical size of the file, otherwise we use
  445.     # the RSS enclosure tag values.
  446.     def getSizeForDisplay(self):
  447.         fname = self.getFilename()
  448.         if fname != "" and os.path.exists(fname):
  449.             size = os.stat(fname)[6]
  450.             return self.sizeFormattedForDisplay(size)
  451.         else:
  452.             return self.getEnclosuresSize()
  453.     
  454.     ##
  455.     # Returns the total size of all enclosures in bytes
  456.     def getEnclosuresSize(self):
  457.         size = 0
  458.         try:
  459.             if self.entry.has_key('enclosures'):
  460.                 enclosures = self.entry['enclosures']
  461.                 for enclosure in enclosures:
  462.                     size += int(enclosure['length'])
  463.         except:
  464.             pass
  465.         return self.sizeFormattedForDisplay(size)
  466.  
  467.     ##
  468.     # returns status of the download in plain text
  469.     def getTotalSize(self):
  470.         size = 0
  471.         for dler in self.downloaders:
  472.             try:
  473.                 size += dler.getTotalSize()
  474.             except:
  475.                 pass
  476.         if size == 0:
  477.             return ""
  478.         return self.sizeFormattedForDisplay(size)
  479.  
  480.     ##
  481.     # returns status of the download in plain text
  482.     def getCurrentSize(self):
  483.         size = 0
  484.         for dler in self.downloaders:
  485.             size += dler.getCurrentSize()
  486.         if size == 0:
  487.             return ""
  488.         return self.sizeFormattedForDisplay(size)
  489.  
  490.     ##
  491.     # Returns a byte size formatted for display
  492.     def sizeFormattedForDisplay(self, bytes, emptyForZero=True):
  493.         if bytes > (1 << 30):
  494.             return "%1.1fGB" % (bytes / (1024.0 * 1024.0 * 1024.0))
  495.         elif bytes > (1 << 20):
  496.             return "%1.1fMB" % (bytes / (1024.0 * 1024.0))
  497.         elif bytes > (1 << 10):
  498.             return "%1.1fKB" % (bytes / 1024.0)
  499.         elif bytes > 1:
  500.             return "%0.0fB" % bytes
  501.         else:
  502.             if emptyForZero:
  503.                 return ""
  504.             else:
  505.                 return "n/a"
  506.  
  507.     ##
  508.     # Returns the download progress in absolute percentage [0.0 - 100.0].
  509.     def downloadProgress(self):
  510.         progress = 0
  511.         self.beginRead()
  512.         try:
  513.             size = 0
  514.             dled = 0
  515.             for dler in self.downloaders:
  516.                 try:
  517.                     size += dler.getTotalSize()
  518.                     dled += dler.getCurrentSize()
  519.                 except:
  520.                     pass
  521.             if size > 0:
  522.                 progress = (100.0*dled) / size
  523.         finally:
  524.             self.endRead()
  525.         return progress
  526.  
  527.     ##
  528.     # Returns the width of the progress bar corresponding to the current
  529.     # download progress. This doesn't really belong here and even forces
  530.     # to use a hardcoded constant, but the templating system doesn't 
  531.     # really leave any other choice.
  532.     def downloadProgressWidth(self):
  533.         fullWidth = 92  # width of resource:channelview-progressbar-bg.png - 2
  534.         progress = self.downloadProgress() / 100.0
  535.         if progress == 0:
  536.             return 0
  537.         return int(progress * fullWidth)
  538.  
  539.     ##
  540.     # Returns string containing three digit percent finished
  541.     # "000" through "100".
  542.     def threeDigitPercentDone(self):
  543.         return '%03d' % int(self.downloadProgress())
  544.  
  545.     ##
  546.     # Returns string with estimate time until download completes
  547.     def downloadETA(self):
  548.         secs = 0
  549.         for dler in self.downloaders:
  550.             secs += dler.getETA()
  551.         if secs == 0:
  552.             return 'starting up...'
  553.         elif (secs < 120):
  554.             return '%1.0f secs left - ' % secs
  555.         elif (secs < 6000):
  556.             return '%1.0f mins left - ' % ceil(secs/60.0)
  557.         else:
  558.             return '%1.1f hours left - ' % ceil(secs/3600.0)
  559.  
  560.     ##
  561.     # Returns the download rate
  562.     def downloadRate(self):
  563.         rate = 0
  564.         unit = "k/s"
  565.         if len(self.downloaders) > 0:
  566.             for dler in self.downloaders:
  567.                 rate = dler.getRate()
  568.             rate /= len(self.downloaders)
  569.  
  570.         rate /= 1024
  571.         if rate > 1000:
  572.             rate /= 1024
  573.             unit = "m/s"
  574.         if rate > 1000:
  575.             rate /= 1024
  576.             unit = "g/s"
  577.             
  578.         return "%d%s" % (rate, unit)
  579.  
  580.     ##
  581.     # Returns the published date of the item
  582.     def getPubDate(self):
  583.         self.beginRead()
  584.         try:
  585.             try:
  586.                 ret = datetime(*self.entry.modified_parsed[0:7]).strftime("%b %d %Y")
  587.             except:
  588.                 ret = ""
  589.         finally:
  590.             self.endRead()
  591.         return ret
  592.     
  593.     ##
  594.     # Returns the published date of the item as a datetime object
  595.     def getPubDateParsed(self):
  596.         self.beginRead()
  597.         try:
  598.             try:
  599.                 ret = datetime(*self.entry.modified_parsed[0:7])
  600.             except:
  601.                 ret = datetime.max # Is this reasonable? It should
  602.                                    # avoid type issues for now, if
  603.                                    # nothing else
  604.         finally:
  605.             self.endRead()
  606.         return ret
  607.  
  608.     ##
  609.     # returns the date this video was released or when it was published
  610.     def getReleaseDate(self):
  611.         try:
  612.             return self.releaseDate
  613.         except:
  614.             try:
  615.                 self.releaseDate = datetime(*self.entry.enclosures[0].modified_parsed[0:7]).strftime("%b %d %Y")
  616.                 return self.releaseDate
  617.             except:
  618.                 try:
  619.                     self.releaseDate = datetime(*self.entry.modified_parsed[0:7]).strftime("%b %d %Y")
  620.                     return self.releaseDate
  621.                 except:
  622.                     self.releaseDate = ""
  623.                     return self.releaseDate
  624.             
  625.  
  626.     ##
  627.     # returns the date this video was released or when it was published
  628.     def getReleaseDateObj(self):
  629.         if hasattr(self,'releaseDateObj'):
  630.             return self.releaseDateObj
  631.         self.beginRead()
  632.         try:
  633.             try:
  634.                 self.releaseDateObj = datetime(*self.entry.enclosures[0].modified_parsed[0:7])
  635.             except:
  636.                 try:
  637.                     self.releaseDateObj = datetime(*self.entry.modified_parsed[0:7])
  638.                 except:
  639.                     self.releaseDateObj = datetime.min
  640.         finally:
  641.             self.endRead()
  642.         return self.releaseDateObj
  643.  
  644.     ##
  645.     # returns string with the play length of the video
  646.     def getDuration(self, emptyIfZero=True):
  647.         secs = 0
  648.         #FIXME get this from VideoInfo
  649.         if secs == 0:
  650.             if emptyIfZero:
  651.                 return ""
  652.             else:
  653.                 return "n/a"
  654.         if (secs < 120):
  655.             return '%1.0f secs' % secs
  656.         elif (secs < 6000):
  657.             return '%1.0f mins' % ceil(secs/60.0)
  658.         else:
  659.             return '%1.1f hours' % ceil(secs/3600.0)
  660.  
  661.     ##
  662.     # returns string with the format of the video
  663.     KNOWN_MIME_TYPES = ('audio', 'video')
  664.     KNOWN_MIME_SUBTYPES = ('mov', 'wmv', 'mp4', 'mp3', 'mpg', 'mpeg', 'avi')
  665.     def getFormat(self, emptyForUnknown=True):
  666.         try:
  667.             enclosure = self.entry['enclosures'][0]
  668.             if enclosure.has_key('type') and len(enclosure['type']) > 0:
  669.                 type, subtype = enclosure['type'].split('/')
  670.                 if type.lower() in self.KNOWN_MIME_TYPES:
  671.                     return subtype.split(';')[0].upper()
  672.             else:
  673.                 extension = enclosure['url'].split('.')[-1].lower()
  674.                 if extension in self.KNOWN_MIME_SUBTYPES:
  675.                     return extension.upper()
  676.         except:
  677.             pass
  678.         if emptyForUnknown:
  679.             return ""
  680.         else:
  681.             return "n/a"
  682.  
  683.     ##
  684.     # return keyword tags associated with the video separated by commas
  685.     def getTags(self):
  686.         self.beginRead()
  687.         try:
  688.             try:
  689.                 ret = self.entry.categories.join(", ")
  690.             except:
  691.                 ret = ""
  692.         finally:
  693.             self.endRead()
  694.         return ret
  695.  
  696.     ##
  697.     # return the license associated with the video
  698.     def getLicence(self):
  699.         self.beginRead()
  700.         try:
  701.             try:
  702.                 ret = self.entry.license
  703.             except:
  704.                 try:
  705.                     ret = self.feed.getLicense()
  706.                 except:
  707.                     ret = ""
  708.         finally:
  709.             self.endRead()
  710.         return ret
  711.  
  712.     ##
  713.     # return the people associated with the video, separated by commas
  714.     def getPeople(self):
  715.         ret = []
  716.         self.beginRead()
  717.         try:
  718.             try:
  719.                 for role in self.entry.enclosures[0].roles:
  720.                     for person in self.entry.enclosures[0].roles[role]:
  721.                         ret.append(person)
  722.                 for role in self.entry.roles:
  723.                     for person in self.entry.roles[role]:
  724.                         ret.append(person)
  725.             except:
  726.                 pass
  727.         finally:
  728.             self.endRead()
  729.         return ', '.join(ret)
  730.  
  731.     ##
  732.     # returns the URL of the webpage associated with the item
  733.     def getLink(self):
  734.         self.beginRead()
  735.         try:
  736.             try:
  737.                 ret = self.entry.link
  738.             except:
  739.                 ret = ""
  740.         finally:
  741.             self.endRead()
  742.         return ret
  743.  
  744.     ##
  745.     # returns the URL of the payment page associated with the item
  746.     def getPaymentLink(self):
  747.         self.beginRead()
  748.         try:
  749.             try:
  750.                 ret = self.entry.enclosures[0].payment_url
  751.             except:
  752.                 try:
  753.                     ret = self.entry.payment_url
  754.                 except:
  755.                     ret = ""
  756.         finally:
  757.             self.endRead()
  758.         return ret
  759.  
  760.     ##
  761.     # returns a snippet of HTML containing a link to the payment page
  762.     # HTML has already been sanitized by feedparser
  763.     def getPaymentHTML(self):
  764.         self.beginRead()
  765.         try:
  766.             try:
  767.                 ret = self.entry.enclosures[0].payment_html
  768.             except:
  769.                 try:
  770.                     ret = self.entry.payment_html
  771.                 except:
  772.                     ret = ""
  773.         finally:
  774.             self.endRead()
  775.         # feedparser returns escaped CDATA so we either have to change its
  776.         # behavior when it parses dtv:paymentlink elements, or simply unescape
  777.         # here...
  778.         return '<span>' + unescape(ret) + '</span>'
  779.  
  780.     ##
  781.     # Updates an item with new data
  782.     #
  783.     # @param entry a dict object containing the new data
  784.     def update(self, entry):
  785.         self.beginChange()
  786.         try:
  787.             self.entry = entry
  788.         finally:
  789.             self.endChange()
  790.  
  791.     ##
  792.     # marks the item as having been downloaded now
  793.     def setDownloadedTime(self):
  794.         self.beginRead()
  795.         try:
  796.             self.downloadedTime = datetime.now()
  797.  
  798.             # Hack to immediately "save" items in feeds set to never expire
  799.             self.keep = (self.feed.expire == "never")
  800.         finally:
  801.             self.endRead()
  802.  
  803.     ##
  804.     # gets the time the video was downloaded
  805.     # Only valid if the state of this item is "finished"
  806.     def getDownloadedTime(self):
  807.         self.beginRead()
  808.         try:
  809.             try:
  810.                 ret = self.downloadedTime
  811.             except:
  812.                 ret = datetime.min
  813.         finally:
  814.             self.endRead()
  815.         return ret
  816.  
  817.     ##
  818.     # gets the time the video started downloading
  819.     def getDLStartTime(self):
  820.         self.beginRead()
  821.         try:
  822.             try:
  823.                 ret = self.DLStartTime
  824.             except:
  825.                 ret = None
  826.         finally:
  827.             self.endRead()
  828.         return ret
  829.  
  830.     ##
  831.     # Returns the filename of the first downloaded video or the empty string
  832.     def getFilename(self):
  833.         self.beginRead()
  834.         try:
  835.             try:
  836.                 ret = self.downloaders[0].getFilename()
  837.             except:
  838.                 ret = ""
  839.         finally:
  840.             self.endRead()
  841.         return ret
  842.  
  843.     ##
  844.     # Returns a list with the filenames of all of the videos in the item
  845.     def getFilenames(self):
  846.         ret = []
  847.         self.beginRead()
  848.         try:
  849.             try:
  850.                 for dl in self.downloaders:
  851.                     ret.append(dl.getFilename())
  852.             except:
  853.                 pass
  854.         finally:
  855.             self.endRead()
  856.         return ret
  857.  
  858.     def getRSSEntry(self):
  859.         self.beginRead()
  860.         try:
  861.             ret = self.entry
  862.         finally:
  863.             self.endRead()
  864.         return ret
  865.  
  866.     def remove(self):
  867.         for dler in self.downloaders:
  868.             dler.remove()
  869.         DDBObject.remove(self)
  870.  
  871.     ##
  872.     # Called by pickle during serialization
  873.     def __getstate__(self):
  874.         temp = copy(self.__dict__)
  875.         temp['dlFactory'] = None
  876.         return (3,temp)
  877.  
  878.     ##
  879.     # Called by pickle during serialization
  880.     def __setstate__(self,state):
  881.         (version, data) = state
  882.         if version == 0:
  883.             data['pendingManualDL'] = False
  884.             if not data.has_key('linkNumber'):
  885.                 data['linkNumber'] = 0
  886.             version += 1
  887.         if version == 1:
  888.             data['keep'] = False
  889.             data['pendingReason'] = ""
  890.             version += 1
  891.         if version == 2:
  892.             data['creationTime'] = datetime.now()
  893.             version += 1
  894.         assert(version == 3)
  895.         data['startingDownload'] = False
  896.         self.__dict__ = data
  897.  
  898.         # Older versions of the database allowed Feed Implementations
  899.         # to act as feeds. If that's the case, change feed attribute
  900.         # to contain the actual feed.
  901.         # NOTE: This assumes that the feed object is decoded
  902.         # before its items. That appears to be generally true
  903.         if not issubclass(self.feed.__class__, DDBObject):
  904.             try:
  905.                 self.feed = self.feed.ufeed
  906.             except:
  907.                 self.__class__ = DropItLikeItsHot
  908.             if self.__class__ is FileItem:
  909.                 self.__class__ = DropItLikeItsHot
  910.  
  911.         self.dlFactory = DownloaderFactory(self)
  912.  
  913. #Dummy class for removing bogus FileItem instances
  914. class DropItLikeItsHot:
  915.     __DropMeLikeItsHot = True
  916.     def __slurp(self, *args, **kwargs):
  917.         pass
  918.     def __getattr__(self, attr):
  919.         if attr == '__DropMeLikeItsHot':
  920.             return self.__DropMeLikeItsHot
  921.         else:
  922.             print "DTV: WARNING! Attempt to call '%s' on DropItLikeItsHot instance" % attr
  923.             return self.__slurp
  924.  
  925. ##
  926. # An Item that exists as a file, but not as a download
  927. class FileItem(Item):
  928.     def getEntry(self,filename):
  929.         return FeedParserDict({'title':os.path.basename(filename),'enclosures':[{'url':filename}]})
  930.  
  931.     def __init__(self,feed,filename):
  932.         Item.__init__(self,feed,self.getEntry(filename))
  933.         self.filename = filename
  934.  
  935.     def getState(self):
  936.         return "saved"
  937.  
  938.     def expire(self):
  939.         try:
  940.             os.remove(self.filename)
  941.         except:
  942.             pass
  943.         self.remove()
  944.  
  945.     def getDownloadedTime(self):
  946.         self.beginRead()
  947.         try:
  948.             try:
  949.                 time = datetime.fromtimestamp(os.getctime(self.filename))
  950.             except:
  951.                 return datetime.min
  952.         finally:
  953.             self.endRead()
  954.  
  955.     def getFilename(self):
  956.         ret = ''
  957.         try:
  958.             ret = self.filename
  959.         except:
  960.             pass
  961.         return ret
  962.  
  963.     def getFilenames(self):
  964.         ret = []
  965.         try:
  966.             ret = [self.filename]
  967.         except:
  968.             pass
  969.         return ret
  970.